1. 组件的本质:虚拟 DOM 节点

在 Vue.js 中,组件本质上是一种特殊的虚拟 DOM 节点。虚拟 DOM(VNode)通过 type 属性来区分不同的节点类型。例如:

  • 普通标签:vnode.type 为字符串,如 'div'
  • 文本节点:vnode.typeText
  • 片段(Fragment):vnode.typeFragment
  • 组件:vnode.type 为组件的选项对象。

例如,一个组件的虚拟 DOM 表示如下:

const MyComponent = {
  name: 'MyComponent',
  data() {
    return { foo: 1 }
  },
  render() {
    return { type: 'div', children: `foo 的值是: ${this.foo}` }
  }
}

const vnode = {
  type: MyComponent // 组件选项对象
}

渲染器通过检查 vnode.type 的类型来决定如何处理节点。当 type 是一个对象时,渲染器识别其为组件,并调用 mountComponentpatchComponent 函数来完成挂载或更新。

渲染器的核心逻辑

渲染器的 patch 函数是处理虚拟 DOM 的核心入口,它根据 vnode.type 的类型分发处理逻辑:

function patch(n1, n2, container, anchor) {
  if (n1 && n1.type !== n2.type) {
    unmount(n1)
    n1 = null
  }

  const { type } = n2
  if (typeof type === 'string') {
    // 处理普通元素
  } else if (type === Text) {
    // 处理文本节点
  } else if (type === Fragment) {
    // 处理片段
  } else if (typeof type === 'object') {
    // 处理组件
    if (!n1) {
      mountComponent(n2, container, anchor)
    } else {
      patchComponent(n1, n2, anchor)
    }
  }
}

对于组件类型,mountComponent 负责初次挂载,patchComponent 负责更新。这种设计让渲染器能够统一处理不同类型的节点。

2. 组件的挂载与渲染

组件的挂载由 mountComponent 函数完成,其核心步骤是:

  1. 获取组件的选项对象(如 render 函数和 data 函数)。
  2. 执行 render 函数生成虚拟 DOM(子树,subTree)。
  3. 调用 patch 函数将子树渲染为真实 DOM。

以下是 mountComponent 的简化实现:

function mountComponent(vnode, container, anchor) {
  const componentOptions = vnode.type
  const { render } = componentOptions
  const subTree = render()
  patch(null, subTree, container, anchor)
}

这个过程展示了组件如何通过 render 函数描述页面内容,并最终通过渲染器转换为真实 DOM。

3. 组件状态与响应式自更新

组件状态的初始化

组件可以通过 data 函数定义自身状态,这些状态需要是响应式的,以便在状态变化时触发更新。例如:

const MyComponent = {
  name: 'MyComponent',
  data() {
    return { foo: 'hello world' }
  },
  render() {
    return { type: 'div', children: `foo 的值是: ${this.foo}` }
  }
}

mountComponent 中,data 函数的返回值会被包装为响应式对象:

function mountComponent(vnode, container, anchor) {
  const { render, data } = vnode.type
  const state = reactive(data())
  const subTree = render.call(state, state)
  patch(null, subTree, container, anchor)
}

通过 reactive 函数,state 成为响应式对象,render 函数通过 this 访问状态数据。

组件的自更新

为了实现状态变化时的自动更新,Vue.js 将组件的渲染任务包装在一个 effect 中:

function mountComponent(vnode, container, anchor) {
  const { render, data } = vnode.type
  const state = reactive(data())

  effect(() => {
    const subTree = render.call(state, state)
    patch(null, subTree, container, anchor)
  })
}

state 发生变化时,effect 会触发 render 函数重新执行,从而更新 DOM。然而,频繁的状态变化可能导致多次渲染,造成性能浪费。为此,Vue.js 使用调度器(scheduler)来优化更新。

调度器的实现

调度器通过微任务队列缓冲渲染任务,避免重复执行:

const queue = new Set()
let isFlushing = false
const p = Promise.resolve()

function queueJob(job) {
  queue.add(job)
  if (!isFlushing) {
    isFlushing = true
    p.then(() => {
      try {
        queue.forEach(job => job())
      } finally {
        isFlushing = false
        queue.clear = 0
      }
    })
  }
}

function mountComponent(vnode, container, anchor) {
  const { render, data } = vnode.type
  const state = reactive(data())

  effect(() => {
    const subTree = render.call(state, state)
    patch(null, subTree, container, anchor)
  }, { scheduler: queueJob })
}

通过 queueJob,渲染任务被放入微任务队列,多次状态变化只会触发一次渲染,显著提升性能。

4. 组件实例与生命周期

组件实例

组件实例是一个对象,维护组件运行时的状态,例如:

  • state:组件的响应式数据。
  • isMounted:标记组件是否已挂载。
  • subTree:组件渲染的子树。

通过组件实例,渲染器可以区分挂载和更新操作:

function mountComponent(vnode, container, anchor) {
  const { render, data } = vnode.type
  const state = reactive(data())
  const instance = {
    state,
    isMounted: false,
    subTree: null
  }
  vnode.component = instance

  effect(() => {
    const subTree = render.call(state, state)
    if (!instance.isMounted) {
      patch(null, subTree, container, anchor)
      instance.isMounted = true
    } else {
      patch(instance.subTree, subTree, container, anchor)
    }
    instance.subTree = subTree
  }, { scheduler: queueJob })
}

生命周期钩子

Vue.js 的生命周期钩子(如 beforeCreatecreated 等)在适当的时机被调用:

function mountComponent(vnode, container, anchor) {
  const { render, data, beforeCreate, created, beforeMount, mounted } = vnode.type
  beforeCreate && beforeCreate()
  const state = reactive(data())
  const instance = { state, isMounted: false, subTree: null }
  vnode.component = instance
  created && created.call(state)

  effect(() => {
    const subTree = render.call(state, state)
    if (!instance.isMounted) {
      beforeMount && beforeMount.call(state)
      patch(null, subTree, container, anchor)
      instance.isMounted = true
      mounted && mounted.call(state)
    } else {
      // 更新逻辑
    }
    instance.subTree = subTree
  }, { scheduler: queueJob })
}

生命周期钩子通过数组存储,支持多个钩子函数的注册。

5. Props 与被动更新

Props 的解析

组件的 props 定义了组件接受的属性,渲染器通过 resolveProps 函数解析 vnode.props 和组件选项中的 props

function resolveProps(options, propsData) {
  const props = {}
  const attrs = {}
  for (const key in propsData) {
    if (key in options || key.startsWith('on')) {
      props[key] = propsData[key]
    } else {
      attrs[key] = propsData[key]
    }
  }
  return [props, attrs]
}

props 是组件显式声明的属性,attrs 是未声明的属性。事件监听器(如 onChange)也被视为 props

被动更新

当父组件的 props 变化时,子组件需要更新。patchComponent 函数处理这种被动更新:

function patchComponent(n1, n2, anchor) {
  const instance = (n2.component = n1.component)
  const { props } = instance
  if (hasPropsChanged(n1.props, n2.props)) {
    const [nextProps] = resolveProps(n2.type.props, n2.props)
    for (const k in nextProps) {
      props[k] = nextProps[k]
    }
    for (const k in props) {
      if (!(k in nextProps)) delete props[k]
    }
  }
}

function hasPropsChanged(prevProps, nextProps) {
  const nextKeys = Object.keys(nextProps)
  if (nextKeys.length !== Object.keys(prevProps).length) {
    return true
  }
  for (let i = 0; i < nextKeys.length; i++) {
    const key = nextKeys[i]
    if (nextProps[key] !== prevProps[key]) return true
  }
  return false
}

通过 shallowReactiveprops 的变化会触发组件重新渲染。

6. Setup 函数与组合式 API

Vue.js 3 引入了 setup 函数,用于组合式 API。它在组件挂载时执行一次,返回值可以是:

  1. 渲染函数:直接作为组件的 render 函数。
  2. 对象:暴露的数据供模板使用。

示例:

const Comp = {
  setup() {
    const count = ref(0)
    return { count }
  },
  render() {
    return { type: 'div', children: `count is: ${this.count}` }
  }
}

setup 函数接收 propssetupContext(包含 attrsemitslots 等):

function mountComponent(vnode, container, anchor) {
  const { render, data, setup } = vnode.type
  const state = data ? reactive(data()) : null
  const [props, attrs] = resolveProps(vnode.type.props, vnode.props)
  const instance = { state, props: shallowReactive(props), isMounted: false, subTree: null }
  const setupContext = { attrs }
  const setupResult = setup(shallowReadonly(instance.props), setupContext)

  let setupState = null
  if (typeof setupResult === 'function') {
    if (render) console.error('setup 返回渲染函数,render 选项将被忽略')
    render = setupResult
  } else {
    setupState = setupResult
  }

  const renderContext = new Proxy(instance, {
    get(t, k) {
      if (k in t.state) return t.state[k]
      if (k in t.props) return t.props[k]
      if (setupState && k in setupState) return setupState[k]
      console.error('属性不存在')
    }
  })
}

7. 自定义事件与 emit

emit 函数用于触发自定义事件。事件名称(如 change)会被转换为 onChange 并存储在 props 中:

function emit(event, ...payload) {
  const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
  const handler = instance.props[eventName]
  if (handler) {
    handler(...payload)
  } else {
    console.error('事件不存在')
  }
}

emit 被添加到 setupContext,供 setup 函数使用。

8. 插槽的实现

插槽允许父组件为子组件提供自定义内容。子组件的模板使用 <slot> 标签,父组件通过 v-slot 指令填充内容。插槽内容被编译为函数,存储在 vnode.children 中:

function mountComponent(vnode, container, anchor) {
  const slots = vnode.children || {}
  const instance = { state, props: shallowReactive(props), isMounted: false, subTree: null, slots }
  const setupContext = { attrs, emit, slots }

  const renderContext = new Proxy(instance, {
    get(t, k) {
      if (k === '$slots') return t.slots
      // 其他逻辑
    }
  })
}

插槽函数的调用结果作为子组件的渲染内容。

9. 生命周期钩子的注册

Vue.js 3 的组合式 API 提供 onMounted 等函数注册生命周期钩子。通过 currentInstance 维护当前组件实例:

let currentInstance = null
function setCurrentInstance(instance) {
  currentInstance = instance
}

function onMounted(fn) {
  if (currentInstance) {
    currentInstance.mounted.push(fn)
  } else {
    console.error('onMounted 只能在 setup 中调用')
  }
}

function mountComponent(vnode, container, anchor) {
  const instance = { state, props: shallowReactive(props), isMounted: false, subTree: null, mounted: [] }
  setCurrentInstance(instance)
  const setupResult = setup(shallowReadonly(instance.props), setupContext)
  setCurrentInstance(null)

  effect(() => {
    const subTree = render.call(renderContext, renderContext)
    if (!instance.isMounted) {
      patch(null, subTree, container, anchor)
      instance.isMounted = true
      instance.mounted.forEach(hook => hook.call(renderContext))
    }
  }, { scheduler: queueJob })
}